iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0

🎯 今天要做什麼?

昨天我們深入了解了斷言的各種用法,今天要學習 TDD 的精髓 —「紅綠重構循環」。

想像一下,你接到一個需求:「我們需要一個判斷質數的函數。」以前你可能直接開始寫程式,但現在我們要用 TDD 的方式:先寫測試(紅燈),再寫最簡實作(綠燈),最後改善代碼(重構)。

學習目標

今天結束後,你將學會:

  • 深度理解 TDD 的三個階段:紅燈、綠燈、重構
  • 掌握每個階段的具體操作和心態
  • 體驗完整的 TDD 開發節奏
  • 學會基本重構手法

TDD 循環的核心理念

TDD 的核心是一個簡單而強大的三步循環:

🔴 紅燈(Red)   ➜  🟢 綠燈(Green)  ➜  🔵 重構(Refactor)
  ↑                                           ↓
  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←
  • 🔴 紅燈階段:寫失敗的測試,表達期望的行為
  • 🟢 綠燈階段:用最簡單的方法讓測試通過
  • 🔵 重構階段:在測試保護下改善代碼品質

🔴 紅燈階段:寫失敗的測試

紅燈階段的核心思想:先思考需求,再動手寫程式

建立 tests/day03/test_math_utils.py

"""數學工具庫測試"""

def test_identifies_small_prime_numbers():
    """識別小質數"""
    # 還沒有 is_prime 函數,所以這個測試會失敗(紅燈)
    assert is_prime(2) == True
    assert is_prime(3) == True
    assert is_prime(5) == True

def test_identifies_small_composite_numbers():
    """識別小合數"""
    assert is_prime(4) == False
    assert is_prime(6) == False
    assert is_prime(9) == False

執行測試:

pytest tests/day03/ -v

預期結果:測試失敗,因為 is_prime 函數還不存在。這就是我們要的「紅燈」!

🟢 綠燈階段:最快速度讓測試通過

綠燈階段的核心思想:用最簡單的方法讓測試通過

建立 src/math/math_utils.py

"""數學工具庫"""

def is_prime(n: int) -> bool:
    """判斷是否為質數"""
    # 最簡單的實作:硬編碼我們測試的數字
    if n in [2, 3, 5]:
        return True
    if n in [4, 6, 9]:
        return False
    return False  # 其他數字先回傳 False

更新測試檔,加入 import:

"""數學工具庫測試"""
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../../src'))

from math.math_utils import is_prime

def test_identifies_small_prime_numbers():
    """識別小質數"""
    assert is_prime(2) == True
    assert is_prime(3) == True
    assert is_prime(5) == True

def test_identifies_small_composite_numbers():
    """識別小合數"""
    assert is_prime(4) == False
    assert is_prime(6) == False
    assert is_prime(9) == False

執行測試:

pytest tests/day03/ -v

結果:測試通過!我們達到了綠燈階段。

🔵 重構階段:改善代碼品質

重構階段的核心思想:在測試保護下,改善代碼品質

我們的硬編碼實作太醜了,讓我們重構:

"""數學工具庫"""

def is_prime(n: int) -> bool:
    """判斷是否為質數"""
    # 處理邊界情況
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    
    # 檢查奇數因子到 sqrt(n)
    i = 3
    while i * i <= n:
        if n % i == 0:
            return False
        i += 2
    
    return True

執行測試確認重構成功:

pytest tests/day03/ -v

測試仍然通過!重構成功。

🚀 完整循環實戰演練

讓我們做第二輪循環,增加邊界情況的測試:

def test_handles_boundary_cases():
    """處理邊界情況"""
    assert is_prime(0) == False
    assert is_prime(1) == False
    assert is_prime(-1) == False

def test_handles_larger_prime_numbers():
    """處理較大質數"""
    assert is_prime(11) == True
    assert is_prime(13) == True

執行測試 - 全部通過!因為我們的重構實作已經正確處理了這些情況。

TDD 開發節奏

TDD 不只是技術,更是一種開發節奏:

  1. 小步快跑:每次只測試一個小功能
  2. 快速反饋:幾分鐘完成一個循環
  3. 持續驗證:每次修改都有測試保護
  4. 漸進改善:通過重構持續提升品質

心理建設要點

  • 紅燈不可怕:失敗指引方向
  • 綠燈要克制:最簡實作,避免過度工程
  • 重構要勇敢:有測試護航,放心改善

重構時機的判斷

看到這些「代碼異味」就該重構了:

  • 重複代碼:相同邏輯出現多次
  • 過長函數:函數做太多事情
  • 魔術數字:硬編碼的數值
  • 不清楚的命名:變數或函數名不明確

常見的重構手法

1. 提取常數

重構前:

if age >= 18:
    # ...

重構後:

MIN_ADULT_AGE = 18
if age >= MIN_ADULT_AGE:
    # ...

2. 重命名變數

重構前:

def calc(x: float) -> float:
    return x * 0.1

重構後:

def calculate_tax(price: float) -> float:
    TAX_RATE = 0.1
    return price * TAX_RATE

💡 pytest 的 TDD 優勢

pytest 讓 TDD 變得更簡潔、更直觀:

簡潔的斷言語法

assert result == expected
assert len(items) > 0
assert "error" in response

豐富的錯誤訊息

>   assert is_prime(4) == True
E   assert False == True
E    +  where False = is_prime(4)

更好的可讀性

與傳統測試框架相比,pytest 的語法更像在描述需求而非寫程式碼。這讓我們在 TDD 的紅燈階段更容易專注於「需求是什麼」,而不是「怎麼寫測試」。

完整實作範例

完整 src/math/math_utils.py

"""數學工具庫"""

def is_prime(n: int) -> bool:
    """判斷是否為質數"""
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    
    i = 3
    while i * i <= n:
        if n % i == 0:
            return False
        i += 2
    
    return True

完整 tests/day03/test_math_utils.py

"""數學工具庫測試"""
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../../src'))

from math.math_utils import is_prime

def test_identifies_small_prime_numbers():
    """識別小質數"""
    assert is_prime(2) == True
    assert is_prime(3) == True
    assert is_prime(5) == True

def test_identifies_small_composite_numbers():
    """識別小合數"""
    assert is_prime(4) == False
    assert is_prime(6) == False
    assert is_prime(9) == False

def test_handles_boundary_cases():
    """處理邊界情況"""
    assert is_prime(0) == False
    assert is_prime(1) == False
    assert is_prime(-1) == False

def test_handles_larger_prime_numbers():
    """處理較大質數"""
    assert is_prime(11) == True
    assert is_prime(13) == True

📝 今天學到什麼?

技術收穫

  • 掌握 TDD 三階段:紅燈寫測試、綠燈快速實作、重構改善品質
  • 理解各階段心態:紅燈專注需求、綠燈專注通過、重構專注品質
  • 體驗開發節奏:小步快跑、快速反饋、持續改善
  • 學會基本重構:提取常數、重命名變數

關鍵要點

  • 紅燈是正常的:失敗的測試指引方向
  • 綠燈要克制:最簡實作就好,不要想太複雜
  • 重構很安全:有測試保護,可以放心改善代碼
  • 小步快跑:每次只改一點點,頻繁執行測試

🎉 總結

TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。

TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。

今日小挑戰 🏆

試著用 TDD 方式實作一個 is_even 函數:

  1. 先寫測試(什麼數字是偶數?)
  2. 最簡實作(讓測試通過)
  3. 重構改善(讓代碼更優雅)

記住 TDD 的節奏:紅燈 → 綠燈 → 重構,小步快跑!

明天我們將學習「測試結構和組織」,了解如何讓測試更清晰、更好維護。


上一篇
Day 02 - 認識斷言(Assertions) 🚀
下一篇
Day 04 - 測試結構與組織 🚀
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言